#include "curve.h"
#include "curvegui.h"

//==============================================================================
/*
    This function must be implemented to create the actual plugin object that
    you want to use.
*/
PizAudioProcessor* JUCE_CALLTYPE createPluginFilter() 
{
    return new MidiCurve();
}

//==============================================================================
MidiCurvePrograms::MidiCurvePrograms ()
: ValueTree("midiCurveValues")
{
	for (int p=0;p<kNumPrograms;p++) 
	{
		ValueTree progv("ProgValues");
		progv.setProperty("progIndex",p,0);

		progv.setProperty("x0",0.f,0);
		progv.setProperty("y0",0.f,0);
		progv.setProperty("PointType0",1.f,0);
		progv.setProperty("x1",64.f*fmidiScaler,0);
		progv.setProperty("y1",64.f*fmidiScaler,0);
		progv.setProperty("PointType1",1.f,0);
		for (int i=2;i<MAX_ENVELOPE_POINTS-1;i++) {
			progv.setProperty("x"+String(i),(float)roundToInt(127.f*(float)i/(MAX_ENVELOPE_POINTS-1))*fmidiScaler,0);
			progv.setProperty("y"+String(i),(float)roundToInt(127.f*(float)i/(MAX_ENVELOPE_POINTS-1))*fmidiScaler,0);
			progv.setProperty("PointType" + String(i),0.f,0);
		}
		progv.setProperty("x"+String(MAX_ENVELOPE_POINTS-1),1.f,0);
		progv.setProperty("y"+String(MAX_ENVELOPE_POINTS-1),1.f,0);
		progv.setProperty("PointType"+String(MAX_ENVELOPE_POINTS-1),1.f,0);

		progv.setProperty("CC",0.f,0); 
		progv.setProperty("CCNumber",1.f*fmidiScaler,0); 
		progv.setProperty("Velocity",1.f,0); 
		progv.setProperty("Aftertouch",0.f,0); 
		progv.setProperty("ChannelPressure",0.f,0);
		progv.setProperty("PitchBend",0.f,0);
		progv.setProperty("Channel",0.f,0);

		progv.setProperty("Name","Program " + String(p+1),0);
		progv.setProperty("lastUIWidth",600,0);
		progv.setProperty("lastUIHeight",400,0);

		this->addChild(progv,p,0);
	}
}

//==============================================================================
MidiCurve::MidiCurve() 
{
    // create built-in programs
    programs = new MidiCurvePrograms;
    String defaultBank = String::empty;
    loadDefaultFxb();

	init = true;
    setCurrentProgram (0);
}

MidiCurve::~MidiCurve() 
{
    if (programs) delete programs;
}

//==============================================================================
float MidiCurve::getParameter (int index) 
{
	if (index<getNumParameters())
		return param[index];
	return 0.f;
}

void MidiCurve::setParameter (int index, float newValue) 
{
	if (index<getNumParameters()) {
		if (param[index] != newValue) {
			if (index==0) param[index]=0.f;
			else if (index==(MAX_ENVELOPE_POINTS*2-2)) param[index]=1.f;
			else {
				param[index] = newValue;
				programs->set(curProgram,getParameterName(index),newValue);
			}
			updatePath();
			sendChangeMessage();
		}
	}
}

const String MidiCurve::getParameterName (int index) 
{
	if (index<kNumPointParams) {
		if (index%2==0) return "x" + String(index/2);
		return "y" + String(index/2);
	}
	if (index<kNumPointParams+MAX_ENVELOPE_POINTS) {
		return "PointType"+String(index - kActive);
	}
	if (index == kCC) return "CC";
	if (index == kCCNumber) return "CCNumber";
	if (index == kVelocity) return "Velocity";
	if (index == kChannelPressure) return "ChannelPressure";
	if (index == kAftertouch) return "AfterTouch";
    if (index == kChannel) return "Channel";
	if (index == kPitchBend) return "PitchBend";

    return "param"+String(index);
}

const String MidiCurve::getParameterText (int index) 
{
	if (index<kNumPointParams) {
		return String(roundToInt(127.f*param[index]));
	}
	if (index<kNumPointParams+MAX_ENVELOPE_POINTS) {
		if (param[index]<0.5f) return "Off";
		if (param[index]<1.f) return "Control";
		return "Corner";
	}
	if (index == kCC || index == kVelocity || index == kPitchBend
		|| index == kChannelPressure || index == kAftertouch) {
		return param[index]<0.5f ? "Off" : "On";
	}
	if (index == kCCNumber) {
		return String(roundToInt(127.f*param[index]));
	}
    if (index==kChannel) {
        if (roundToInt(param[kChannel]*16.0f)==0) return String("Any");
        return String(roundToInt(param[kChannel]*16.0f));
    }
	return String::empty;
}

const String MidiCurve::getInputChannelName (const int channelIndex) const
{
	if (channelIndex<getNumInputChannels())
		return String(JucePlugin_Name) + String(" ") + String (channelIndex + 1);
	return String::empty;
}

const String MidiCurve::getOutputChannelName (const int channelIndex) const
{
	if (channelIndex<getNumOutputChannels())
	    return String(JucePlugin_Name) + String(" ") + String (channelIndex + 1);
	return String::empty;
}

bool MidiCurve::isInputChannelStereoPair (int index) const
{
    return true;
}

bool MidiCurve::isOutputChannelStereoPair (int index) const
{
    return true;
}


//======================Programs================================================
void MidiCurve::setCurrentProgram (int index)
{
    //save non-parameter info to the old program, except the first time
    if (!init) {
		programs->set(curProgram,"lastUIHeight",lastUIHeight);
		programs->set(curProgram,"lastUIWidth",lastUIWidth);
    }
    init = false;

	if (curProgram!=index) {
		lastMsg.lastCCIn=-1;
		lastMsg.lastCCOut=-1;
	}
    //then set the new program
    curProgram = index;
	programs->setProperty("lastProgram",index,0);
	resetPoints(false);
    for (int i=0;i<getNumParameters();i++) {
		param[i] = programs->get(index,getParameterName(i));
    }
    lastUIWidth = programs->get(index,"lastUIWidth");
    lastUIHeight = programs->get(index,"lastUIHeight");

	updatePath();
    sendChangeMessage();    
}

void MidiCurve::changeProgramName(int index, const String &newName) {
	if (index<getNumPrograms())
		programs->set(index,"Name",newName);
}

const String MidiCurve::getProgramName(int index) {
	if (index<getNumPrograms())
		return programs->get(index,"Name");
	return String::empty;
}

int MidiCurve::getCurrentProgram() {
    return curProgram;
}

//==============================================================================
void MidiCurve::prepareToPlay (double sampleRate, int samplesPerBlock)
{
    // do your pre-playback setup stuff here..
}

void MidiCurve::releaseResources()
{
    // when playback stops, you can use this as an opportunity to free up any
    // spare memory, etc.
}

float MidiCurve::getPointValue(int n, int y) 
{
	if (n<MAX_ENVELOPE_POINTS) 
	{
		if (!y) 
			return points[n].p.getX();
		else 
			return 1.f-points[n].p.getY();
	}
	return -1.f;
}

float MidiCurve::findValue(float input) 
{
	PathFlatteningIterator it(path,AffineTransform::identity,fmidiScaler);
	while (it.next()) {
		if (it.x1==input) return 1.f-it.y1;
		if (it.x2>=input) {
			return 1.f-(float)linearInterpolate(input,it.y1,it.y2,it.x1,it.x2);
		}
	}

	return -1.f;
}

double MidiCurve::linearInterpolate(double x,double y1,double y2,double x1,double x2) {
      double slope = (y2-y1)/(x2-x1);
      double y0 = y1 - slope * x1;
      return slope * x + y0;
}

void MidiCurve::processBlock (AudioSampleBuffer& buffer,
                                   MidiBuffer& midiMessages)
{
    for (int i = getNumInputChannels(); i < getNumOutputChannels(); ++i)
    {
        buffer.clear (i, 0, buffer.getNumSamples());
    }
    const int channel = roundToInt(param[kChannel]*16.0f);
	MidiBuffer output;
    MidiBuffer::Iterator mid_buffer_iter(midiMessages);
    MidiMessage midi_message(0xFE);
    int sample_number;
    while(mid_buffer_iter.getNextEvent(midi_message,sample_number)) {
		if (midi_message.isProgramChange()) {
			setCurrentProgram(midi_message.getProgramChangeNumber());
		}
		if (channel==0 || midi_message.isForChannel(channel)) 
		{
			if (midi_message.isController()) {
				if (param[kCC]>=0.5f && midi_message.getControllerNumber()==roundToInt(param[kCCNumber]*127.f))
				{
					int v = midi_message.getControllerValue();
					uint8* data = midi_message.getRawData();
					lastMsg.lastCCIn = v;
					v=roundToInt(127.f * findValue(v*fmidiScaler));
					lastMsg.lastCCOut = v;
					MidiMessage cc = MidiMessage(data[0],data[1],v);
					output.addEvent(cc,sample_number);
					lastMsg.sendChangeMessage();
				}
				else output.addEvent(midi_message,sample_number);
			}
			else if (midi_message.isNoteOn()) {
				if (param[kVelocity]>=0.5f)
				{
					int v = midi_message.getVelocity();
					uint8* data = midi_message.getRawData();
					lastMsg.lastCCIn = v;
					v=roundToInt(127.f * findValue(v*fmidiScaler));
					lastMsg.lastCCOut = v;
					MidiMessage cc = MidiMessage(data[0],data[1],v);
					output.addEvent(cc,sample_number);
					lastMsg.sendChangeMessage();
				}
				else output.addEvent(midi_message,sample_number);
			}
			else if (midi_message.isChannelPressure()) {
				if (param[kChannelPressure]>=0.5f)
				{
					int v = midi_message.getChannelPressureValue();
					uint8* data = midi_message.getRawData();
					lastMsg.lastCCIn = v;
					v=roundToInt(127.f * findValue(v*fmidiScaler));
					lastMsg.lastCCOut = v;
					MidiMessage out = MidiMessage(data[0],v);
					output.addEvent(out,sample_number);
					lastMsg.sendChangeMessage();
				}
				else output.addEvent(midi_message,sample_number);
			}
			else if (midi_message.isAftertouch()) {
				if (param[kAftertouch]>=0.5f)
				{
					int v = midi_message.getAfterTouchValue();
					uint8* data = midi_message.getRawData();
					lastMsg.lastCCIn = v;
					v=roundToInt(127.f * findValue(v*fmidiScaler));
					lastMsg.lastCCOut = v;
					MidiMessage out = MidiMessage(data[0],data[1],v);
					output.addEvent(out,sample_number);
					lastMsg.sendChangeMessage();
				}
				else output.addEvent(midi_message,sample_number);
			}
			else if (midi_message.isPitchWheel()) {
				if (param[kPitchBend]>=0.5f)
				{
					int v = midi_message.getPitchWheelValue();
					uint8* data = midi_message.getRawData();
					lastMsg.lastCCIn = (v & 0x3f80)>>7;
					v = findPBValue(v);
					lastMsg.lastCCOut = (v & 0x3f80)>>7;
					MidiMessage out = MidiMessage(data[0], v & 0x007f, (v & 0x3f80)>>7);
					output.addEvent(out,sample_number);
					lastMsg.sendChangeMessage();
				}
				else output.addEvent(midi_message,sample_number);
			}
			else output.addEvent(midi_message,sample_number);
		}
		else output.addEvent(midi_message,sample_number);
    }
	midiMessages=output;
}

int MidiCurve::findPBValue(int input) {
	float v = (float)input;
	PathFlatteningIterator it(path,AffineTransform::identity,(float)0.0000610388);
	while (it.next()) {
		if (it.x1==v) return roundToInt(16383.f * (1.f-it.y1));
		if (it.x2>=v) {
			return roundToInt(16383.f * (1.f-(float)linearInterpolate(v,it.y1,it.y2,it.x1,it.x2)));
		}
	}

	return -1;
}

void MidiCurve::updatePath() {
	points.clear();
	for (int i=0;i<MAX_ENVELOPE_POINTS;i++) {
		midiPoint p = midiPoint(param[i*2],param[i*2+1],isPointActive(i),isPointControl(i));
		points.addSorted(pointComparator,p);
	}
    Path myPath;
	if (points[0].isActive) {
	    myPath.startNewSubPath (0.f, getPointValue(0,1));
		myPath.lineTo (0.f,getPointValue(0,1));
	}
	else {
	    myPath.startNewSubPath (0.f, 1.f);
		myPath.lineTo (0.f, 1.f);
	}
	for (int i = 1; i < MAX_ENVELOPE_POINTS; i++) {
		if (points[i].isActive && !points[i].isControl) 
		{
			int prev = getPrevActivePoint(i);
			if (points[prev].isControl) {
				if (points[getPrevActivePoint(prev)].isControl) {
					myPath.cubicTo(getPointValue(getPrevActivePoint(prev),0), getPointValue(getPrevActivePoint(prev),1), 
								   getPointValue(prev,0), getPointValue(prev,1),
				      			   getPointValue(i,0),   getPointValue(i,1));
				}
				else { 
					myPath.quadraticTo(getPointValue(prev,0), getPointValue(prev,1),
				      				   getPointValue(i,0),   getPointValue(i,1));
				}
			}
			else {
				myPath.lineTo (getPointValue(i,0), getPointValue(i,1));
			}
		}
	}
	if (!points[MAX_ENVELOPE_POINTS-1].isActive) {
		myPath.lineTo (1.f, 0.f);
	}
	path=myPath;
}

int MidiCurve::getPrevActivePoint(int currentPoint) {
	for (int i=currentPoint-1;i>0;i--) {
		if (points[i].isActive) return i;
	}
	return 0;
}

int MidiCurve::getNextActivePoint(int currentPoint) {
	for (int i=currentPoint+1;i<MAX_ENVELOPE_POINTS;i++) {
		if (points[i].isActive) return i;
	}
	return MAX_ENVELOPE_POINTS-1;
}

void MidiCurve::resetPoints(bool copyToProgram) {
	for (int i=0;i<MAX_ENVELOPE_POINTS;i++) {
		param[i*2] = (float)roundToInt(127.f*(float)i/(MAX_ENVELOPE_POINTS-1))*fmidiScaler;
		param[i*2+1] = param[i*2];
		param[kActive + i] = 0.f;
	}
	param[kActive]=1.f;
	param[kActive+1]=1.f;
	param[2] = param[3] = 64.f*fmidiScaler;
	param[kActive+MAX_ENVELOPE_POINTS-1]=1.f;
	
	if (copyToProgram)
	{
		for (int i=0;i<getNumParameters();i++)
			 programs->set(curProgram,getParameterName(i),param[i]);
	}
	updatePath();
	sendChangeMessage();
}

bool MidiCurve::isPointActive(int point) 
{
	if(point<0) return false;
	return getParameter(point + kActive) > 0.5f;
}

bool MidiCurve::isPointControl(int point) 
{
	if(point<0) return false;
	return getParameter(point + kActive) > 0.1f && getParameter(point + kActive) < 0.9f;
}

//==============================================================================
AudioProcessorEditor* MidiCurve::createEditor()
{
    return new CurveEditor (this);
}

void MidiCurve::copySettingsToProgram(int index)
{
	for (int i=0;i<getNumParameters();i++)
		programs->set(index,getParameterName(i),param[i]);
    programs->set(index,"Name",getProgramName(index));
	programs->set(index,"lastUIHeight",lastUIHeight);
    programs->set(index,"lastUIWidth",lastUIWidth);
}

//==============================================================================
void MidiCurve::getCurrentProgramStateInformation (MemoryBlock& destData)
{
	copySettingsToProgram(curProgram);
	programs->getChild(curProgram).writeToStream(MemoryOutputStream(destData,false));
}

void MidiCurve::getStateInformation(MemoryBlock &destData) 
{
	copySettingsToProgram(curProgram);
	programs->writeToStream(MemoryOutputStream(destData,false));
}

void MidiCurve::setCurrentProgramStateInformation (const void* data, int sizeInBytes)
{
	//check for old format
    ScopedPointer<XmlElement> xmlState = getXmlFromBinary (data, sizeInBytes);
    if (xmlState != 0)
    {
        // check that it's the right type of xml..
        if (xmlState->hasTagName ("MYPLUGINSETTINGS"))
        {
            // ok, now pull out our parameters..
            changeProgramName(getCurrentProgram(),xmlState->getStringAttribute ("progname", "Default"));
            for (int i=0;i<kNumParams;i++) {
				param[i] = (float) xmlState->getDoubleAttribute (String(i), param[i]);
            }
			lastUIWidth = xmlState->getIntAttribute ("uiWidth", lastUIWidth);
            lastUIHeight = xmlState->getIntAttribute ("uiHeight", lastUIHeight);

			copySettingsToProgram(curProgram);

			updatePath();
            sendChangeMessage();
			this->dispatchPendingMessages();
        }
    }
	else 
	{
		programs->removeChild(programs->getChild(curProgram),0);
		programs->addChild(ValueTree::readFromStream(MemoryInputStream(data,sizeInBytes,false)),curProgram,0);
		programs->getChild(curProgram).setProperty("progIndex",curProgram,0);
		init=true;
		setCurrentProgram(curProgram);
	}
}

void MidiCurve::setStateInformation (const void* data, int sizeInBytes) 
{
    ScopedPointer<XmlElement> xmlState = getXmlFromBinary (data, sizeInBytes);
	if (xmlState != 0) 
	{
        if (xmlState->hasTagName ("MYPLUGINSETTINGS"))
        {
            for (int p=0;p<getNumPrograms();p++) {
                String prefix = "P" + String(p) + "."; 
                for (int i=0;i<kNumParams;i++) {
					programs->set(p,getParameterName(i),(float) xmlState->getDoubleAttribute (prefix+String(i), programs->get(p,getParameterName(i))));
                }
                programs->set(p,"lastUIWidth",xmlState->getIntAttribute (prefix+"uiWidth",programs->get(p,"lastUIWidth")));
                programs->set(p,"lastUIHeight",xmlState->getIntAttribute (prefix+"uiHeight", programs->get(p,"lastUIHeight")));
                programs->set(p,"Name",xmlState->getStringAttribute (prefix+"progname",programs->get(p,"Name")));
            }
            init=true;
            setCurrentProgram(xmlState->getIntAttribute("program", 0));
        }
    }
	else
	{
		ValueTree vt = ValueTree::readFromStream(MemoryInputStream(data,sizeInBytes,false));
		if (vt.isValid())
		{
			programs->removeAllChildren(0);
			for (int i=0;i<vt.getNumChildren();i++)
			{
				programs->addChild(vt.getChild(i).createCopy(),i,0);
			}
		}
		init=true;
		setCurrentProgram(vt.getProperty("lastProgram",0));
	}
}